/**
 * Copyright 2009 Jonas Ådahl.
 * Copyright 2011 Florian Schmaus			 
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smackx.entitycaps;

import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.filter.PacketExtensionFilter;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.util.Base64;
import org.jivesoftware.smackx.FormField;
import org.jivesoftware.smackx.ServiceDiscoveryManager;
import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
import org.jivesoftware.smackx.provider.CapsExtensionProvider;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DataForm;
import org.jivesoftware.smackx.packet.DiscoverInfo.Feature;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * Keeps track of entity capabilities.
 */
public class EntityCapsManager {

	public static final String HASH_METHOD = "sha-1";
	public static final String HASH_METHOD_CAPS = "SHA-1";

	// TODO entityNode should become a constant (final)
	private static String entityNode = "http://www.igniterealtime.org/projects/smack/";
	private static EntityCapsPersistentCache persistentCache;

	private ServiceDiscoveryManager sdm;

	/**
	 * Map of (node, hash algorithm) -&gt; DiscoverInfo.
	 */
	private static Map<String, DiscoverInfo> caps = new ConcurrentHashMap<String, DiscoverInfo>();

	/**
	 * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the
	 * key is formed as user@server/resource (resource is required) In case of
	 * link-local connection the key is formed as user@host (no resource) In
	 * case of a server or component the key is formed as domain
	 */
	private Map<String, String> userCaps = new ConcurrentHashMap<String, String>();

	// CapsVerListeners gets notified when the version string is changed.
	private Set<CapsVerListener> capsVerListeners = new CopyOnWriteArraySet<CapsVerListener>();

	private String currentCapsVersion = null;
	private boolean sendPresence = false;

	static {
		ProviderManager.getInstance().addExtensionProvider(
				CapsExtension.NODE_NAME, CapsExtension.XMLNS,
				new CapsExtensionProvider());
	}

	/**
	 * Add DiscoverInfo to the database.
	 * 
	 * @param node
	 *            The node name. Could be for example
	 *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=".
	 * @param info
	 *            DiscoverInfo for the specified node.
	 */
	public static void addDiscoverInfoByNode(String node, DiscoverInfo info) {
		cleanupDicsoverInfo(info);

		caps.put(node, info);

		if (persistentCache != null)
			persistentCache.addDiscoverInfoByNodePersistent(node, info);
	}

	public EntityCapsManager(ServiceDiscoveryManager sdm) {
		// Add Entity Capabilities (XEP-0115) feature node.
		sdm.addFeature("http://jabber.org/protocol/caps");

		this.sdm = sdm;
	}

	/**
	 * Add a record telling what entity caps node a user has. The entity caps
	 * node has the format node#ver.
	 * 
	 * @param user
	 *            the user (Full JID)
	 * @param node
	 *            the entity caps node#ver
	 */
	public void addUserCapsNode(String user, String node) {
		if (user != null && node != null) {
			userCaps.put(user, node);
		}
	}

	/**
	 * Remove a record telling what entity caps node a user has.
	 * 
	 * @param user
	 *            the user (Full JID)
	 */
	public void removeUserCapsNode(String user) {
		userCaps.remove(user);
	}

	/**
	 * Get the Node version (node#ver) of a user.
	 * 
	 * @param user
	 *            the user (Full JID)
	 * @return the node version.
	 */
	public String getNodeVersionByUser(String user) {
		return userCaps.get(user);
	}

	/**
	 * Get the discover info given a user name. The discover info is returned if
	 * the user has a node#ver associated with it and the node#ver has a
	 * discover info associated with it.
	 * 
	 * @param user
	 *            user name (Full JID)
	 * @return the discovered info
	 */
	public DiscoverInfo getDiscoverInfoByUser(String user) {
		String capsNode = userCaps.get(user);
		if (capsNode == null)
			return null;

		return getDiscoverInfoByNode(capsNode);
	}

	/**
	 * Get our own caps version.
	 * 
	 * @return our own caps version
	 */
	public String getCapsVersion() {
		return currentCapsVersion;
	}

	/**
	 * Get our own entity node.
	 * 
	 * @return our own entity node.
	 */
	public String getNode() {
		return entityNode;
	}

	/**
	 * Set our own entity node.
	 * 
	 * @param node
	 *            the new node
	 */
	public void setNode(String node) {
		entityNode = node;
	}

	/**
	 * Retrieve DiscoverInfo for a specific node.
	 * 
	 * @param node
	 *            The node name.
	 * @return The corresponding DiscoverInfo or null if none is known.
	 */
	public static DiscoverInfo getDiscoverInfoByNode(String node) {
		return caps.get(node);
	}

	private static void cleanupDicsoverInfo(DiscoverInfo info) {
		info.setFrom(null);
		info.setTo(null);
		info.setPacketID(null);
	}

	public void addPacketListener(Connection connection) {
		PacketFilter f = new AndFilter(new PacketTypeFilter(Presence.class),
				new PacketExtensionFilter(CapsExtension.NODE_NAME,
						CapsExtension.XMLNS));
		connection.addPacketListener(new CapsPacketListener(this), f);
	}

	public void addCapsVerListener(CapsVerListener listener) {
		capsVerListeners.add(listener);

		if (currentCapsVersion != null)
			listener.capsVerUpdated(currentCapsVersion);
	}

	public void removeCapsVerListener(CapsVerListener listener) {
		capsVerListeners.remove(listener);
	}

	private void notifyCapsVerListeners() {
		for (CapsVerListener listener : capsVerListeners) {
			listener.capsVerUpdated(currentCapsVersion);
		}
	}

	/**
	 * Calculate Entity Caps version string
	 * 
	 * @param capsString
	 * @return
	 */
	private static String capsToHash(String capsString) {
		try {
			MessageDigest md = MessageDigest.getInstance(HASH_METHOD_CAPS);
			byte[] digest = md.digest(capsString.getBytes());
			return Base64.encodeBytes(digest);
		} catch (NoSuchAlgorithmException nsae) {
			return null;
		}
	}

	private static String formFieldValuesToCaps(Iterator<String> i) {
		String s = "";
		SortedSet<String> fvs = new TreeSet<String>();
		for (; i.hasNext();) {
			fvs.add(i.next());
		}
		for (String fv : fvs) {
			s += fv + "<";
		}
		return s;
	}

	public void calculateEntityCapsVersion(DiscoverInfo discoverInfo,
			String identityType, String identityName, DataForm extendedInfo) {
		String s = "";

		// Add identity
		// FIXME language
		s += "client/" + identityType + "//" + identityName + "<";

		// Add features
		SortedSet<String> features = new TreeSet<String>();
		for (Iterator<Feature> it = discoverInfo.getFeatures(); it.hasNext();)
			features.add(it.next().getVar());

		for (String f : features) {
			s += f + "<";
		}

		if (extendedInfo != null) {
			synchronized (extendedInfo) {
				SortedSet<FormField> fs = new TreeSet<FormField>(
						new Comparator<FormField>() {
							public int compare(FormField f1, FormField f2) {
								return f1.getVariable().compareTo(
										f2.getVariable());
							}
						});

				FormField ft = null;

				for (Iterator<FormField> i = extendedInfo.getFields(); i
						.hasNext();) {
					FormField f = i.next();
					if (!f.getVariable().equals("FORM_TYPE")) {
						fs.add(f);
					} else {
						ft = f;
					}
				}

				// Add FORM_TYPE values
				if (ft != null) {
					s += formFieldValuesToCaps(ft.getValues());
				}

				// Add the other values
				for (FormField f : fs) {
					s += f.getVariable() + "<";
					s += formFieldValuesToCaps(f.getValues());
				}
			}
		}

		setCurrentCapsVersion(discoverInfo, capsToHash(s));
	}

	/**
	 * Set our own caps version.
	 * 
	 * @param capsVersion
	 *            the new caps version
	 */
	public void setCurrentCapsVersion(DiscoverInfo discoverInfo,
			String capsVersion) {
		currentCapsVersion = capsVersion;
		addDiscoverInfoByNode(getNode() + "#" + capsVersion, discoverInfo);
		notifyCapsVerListeners();
	}

	/**
	 * Set the persistent cache implementation
	 * 
	 * @param cache
	 * @throws IOException
	 */
	public static void setPersistentCache(EntityCapsPersistentCache cache)
			throws IOException {
		if (persistentCache != null)
			throw new IllegalStateException(
					"Entity Caps Persistent Cache was already set");
		persistentCache = cache;
		persistentCache.replay();
	}

	/**
	 * Get a DiscoverInfo for the current entity caps node.
	 * 
	 * @return a DiscoverInfo for the current entity caps node
	 */
	public DiscoverInfo getOwnDiscoverInfo() {
		DiscoverInfo di = new DiscoverInfo();
		di.setType(IQ.Type.RESULT);
		di.setNode(getNode() + "#" + getCapsVersion());

		// Add discover info
		sdm.addDiscoverInfoTo(di);

		return di;
	}

	@SuppressWarnings("unused")
	private void setSendPresence() {
		sendPresence = true;
	}

	public boolean isSendPresence() {
		return sendPresence;
	}
}
